

class VendorPluginStandardOfflineComponent(Component):

    def __init__(self, sysSnapshot, vendorPlugin, componentInfo, serial, componentSaveId, offlineReason):
        # create instance
        super().__init__(vendorPlugin.get_id(),
                         componentInfo["model"],
                         ComponentType(componentInfo["component_type"]),
                         serial,
                         None,                  # id
                         componentSaveId,       # saveId
                         offlineReason)         # offlineReason

        # add devices
        for componentDeviceInfo in componentInfo.get("devices", []):
            # all devices in offline component are dumb device
            obj = Device(None,
                         None,
                         DeviceType(componentDeviceInfo["device_type"]),
                         componentDeviceInfo.get("id", "0"))
            self._addDevice(obj)

        # add connectors
        for componentConnectorInfo in componentInfo.get("connectors", []):
            # all devices in offline component are dumb device
            obj = Connector(ConnectorType(componentConnectorInfo["connector_type"]),
                            componentConnectorInfo.get("id", "0"))
            self._addConnector(obj)

    def _callEvaluate(self, sys_snapshot, parent):
        assert self._parent is None

        hint = self._hint.copy()
        assert "parent_is_online" not in hint
        assert "parent_offline_reason" not in hint
        if isinstance(parent, Machine):
            self._hint["parent_is_online"] = True
        elif isinstance(parent, Component):
            self._hint["parent_is_online"] = self._parent.is_online()
            if self._parent.is_online():
                self._hint["parent_offline_reason"] = self._parent.get_offline_reason()
        else:
            assert False

        self._realId, self._realLocation, self._realPeerConnectorMatcher, self._realOfflineReason = self._evaluate(sys_snapshot, self._hint)
        self._parent = parent

        if "location" in self._hint:
            assert self._realLocation == self._hint["location"]

        if "peer_connector_matcher" in self._hint:
            assert self._realPeerConnectorMatcher == self._hint["peer_connector_matcher"]

        if "is_online" in self._hint:
            if self._hint["is_online"]:
                assert self._realOfflineReason is None
            else:
                assert isinstance(self._realOfflineReason, OfflineReason)
                assert self._realOfflineReason not in [OfflineReason.POWER_OFF, OfflineReason.REMOVED]


    def set_peer_connector_matcher(self, peer_connector_matcher, pre_ready_call=False):
        assert pre_ready_call
        assert self._getMachine() is None

        # do set
        if peer_connector_matcher == self._peerConnectorMatcher:
            return
        self._peerConnectorMatcher = peer_connector_matcher

    def _callEvaluate(self, sys_snapshot, parent):
        assert self._parent is None

        hint = self._hint.copy()
        assert "parent_id" not in hint
        assert "parent_is_online" not in hint
        assert "parent_offline_reason" not in hint
        if isinstance(parent, Machine):
            self._hint["parent_id"] = None
            self._hint["parent_is_online"] = True
        elif isinstance(parent, Component):
            self._hint["parent_id"] = parent.get_id()
            self._hint["parent_is_online"] = self._parent.is_online()
            if self._parent.is_online():
                self._hint["parent_offline_reason"] = self._parent.get_offline_reason()
        else:
            assert False

        self._realId, self._realLocation, self._realOfflineReason = self._evaluate(sys_snapshot, self._hint)
        self._parent = parent

        if "location" in self._hint:
            assert self._realLocation == self._hint["location"]

        if "is_online" in self._hint:
            if self._hint["is_online"]:
                assert self._realOfflineReason is None
            else:
                assert isinstance(self._realOfflineReason, OfflineReason)
                assert self._realOfflineReason not in [OfflineReason.POWER_OFF, OfflineReason.REMOVED]


    def set_hint(self, key, value):
        assert self._parent is None

        assert value is not None
        self._hint[key] = value

        # read:  self.get_hint()
        # write: self.set_hint()
        #        Can do the above operation only when this component is NOT attached to machine
        #        User hints: location:str
        #                    need_offline_state:bool
        #                    is_online:bool
        #                    offline_reason:enum
        #        Inner hints:
        self._hint = dict()



        # read:  self.get_hint()
        # write: self.set_hint()
        #        Can do the above operation only when this component is NOT attached to machine
        #        User hints: id:str
        #                    location:str
        #                    is_online:bool
        #                    offline_reason:enum
        #        Inner hints: parent_is_on_line:bool
        #                     parent_offline_reason:enum
        self._hint = dict()



    def _callEvaluate(self, sys_snapshot, parent):
        assert self._parent is None

        self._realId, self._realLocation, self._realPersistent, self._realOfflineReason = self._evaluate(sys_snapshot, self._hint)
        self._parent = parent

        if "location" in self._hint:
            assert self._realLocation == self._hint["location"]

        if "need_offline_state" in self._hint:
            assert self._realPersistent == self._hint["need_offline_state"]

        if "is_online" in self._hint:
            if self._hint["is_online"]:
                assert self._realOfflineReason is None
            else:
                assert isinstance(self._realOfflineReason, OfflineReason)

        for d in self._devices:
            ComponentDeviceConnectorPropertiesFile.modify_device(d)
            d._callEvaluate(sys_snapshot, self)

        for x in self._connectors:
            ComponentDeviceConnectorPropertiesFile.modify_connector(x)
            x._callEvaluate(sys_snapshot, self)

        self._after_devices_connectors_evaluated(sys_snapshot)





    def supplement_components(self, machine):
        prober = SysSnapshotProber()
        for k, data in self._products["components"].items():
            if "prober" not in data:
                continue
            model, block = k
            print(k)
            prober.add_expression(data["prober"], lambda m=model, b=block: machine.add_component(ComponentDeviceConnectorPropertiesFile.modify_component(self.get_component(m, b)), pre_ready_call=True))
        prober.evaluate(machine._sysSnapshot)




        # use udev to recognize vendor and model
        if vendor is None:
            ret = udevContext.list_devices(path="/devices/virtual/dmi/id")
            if len(ret) > 0:
                assert len(ret) == 1
                ret = ret[0]
                vendor = ret.properties("ID_VENDOR")
                model = ret.properties("ID_MODEL")



    # def get_monitor_layout(self):
    #     pass

    # def get_loudspeaker_layout(self):
    #     pass




        if os.path.exists("/usr/bin/bluetoothctl"):
            for line in subprocess.check_output(["bluetoothctl", "devices"], universal_newlines=True).split("\n"):
                m = re.fullmatch(r"Device (.*) (\S+)", line)
                if m is not None:
                    self._bluetoothDevices.append(self.BluetoothDevice(m.group(2), m.group(1)))

    def get_lmsensors_config_file(self):
        assert self.__ready()
        # FIXME



        # read:  self.get_kernel_config(), self.get_kernel_cmdline_paramter(), self.get_lmsensors_config_file()
        #        Runtime properties, only "attached Device object" can do the above operations
        # write: Runtime properties, not writable
        pass
    








class DevenvDb:

    @staticmethod
    def getDevenv(vendorId, deviceId):
        ret = DevenvDb._doGetDrm(vendorId, deviceId)
        if ret is not None:
            return ret

        return None

    @staticmethod
    def _doGetDrm(vendorId, deviceId):
        # returns: {
        #     "mem": 8,           # unit: GB
        #     "fp64": 760,        # unit: gflops
        #     "fp32": 13200,      # unit: gflops
        #     "fp16": 24500,      # unit: gflops
        # }

        if vendorId == 0x1002 and deviceId == 0x66af:
            # AMD Radeon VII, https://www.amd.com/en/products/graphics/amd-radeon-vii
            return {
                "mem": 16 * 1024 * 1024 * 1024,                # 16GiB
                "fp64": int(3.46 * 1024),                      # 3.46 TFLOPs
                "fp32": int(13.8 * 1024),                      # 13.8 TFLOPs
                "fp16": int(27.7 * 1024),                      # 27.7 TFLOPs
            }

        # unknown device
        return None





class _UtilHwDict:

    @staticmethod
    def get(hwSpec):
        if hwSpec is not None:
            ret = copy.deepcopy(hwSpec)
        else:
            ret = dict()

        _UtilHwDict._getCpuInfo(ret)
        _UtilHwDict._getMemInfo(ret)
        _UtilHwDict._getSensorInfo(ret)
        return ret

    @staticmethod
    def _getCpuInfo(ret):
        buf = pathlib.Path("/proc/cpuinfo").read_text()

        ret["cpu"] = dict()

        ret["cpu"]["vendor"] = "Unknown"
        if True:
            m = re.search(r'vendor_id\s*:\s*(\S+)', buf, re.M)
            if m is not None:
                if m.group(1) == "GenuineIntel":
                    ret["cpu"]["vendor"] = "Intel"
                if m.group(1) == "AuthenticAMD":
                    ret["cpu"]["vendor"] = "AMD"

        ret["cpu"]["model"] = "Unknown"
        if True:
            m = re.search(r'model name\s*:\s*(.*)', buf, re.M)
            if m is not None:
                # intel models
                if "i7-4600U" in m.group(1):
                    ret["cpu"]["model"] = "i7-4600U"

                # amd models
                if "Ryzen Threadripper 1920X" in m.group(1):
                    ret["cpu"]["model"] = "Ryzen Threadripper 1920X"
                if "Ryzen Threadripper 2990WX" in m.group(1):
                    ret["cpu"]["model"] = "Ryzen Threadripper 2990WX"

        ret["cpu"]["cores"] = multiprocessing.cpu_count()

    @staticmethod
    def _getMemInfo(ret):
        ret["memory"] = {
            "size": Util.getPhysicalMemorySize(),         # memory size in GiB
        }

    @staticmethod
    def _getSensorInfo(ret):
        ret["sensor"] = dict()


    def remove_device(self, device, pre_ready_call=False):
        assert self.__readyCheck(pre_ready_call)

        # pre-remove check
        assert device.is_dumb() or not device.is_online()                     # only dumb or offline device can be removed

        # do remove
        device._parent = None
        self._devices.remove(device)

        # FIXME: how to identify which file should be write
        if not pre_ready_call:
            ComponentDeviceFile.write(self._components, self._devices)






    @classmethod
    def modify_device(cls, device):
        cls._read()

        ret = Handy.findItemsByDevice(cls._DEVICES, device)
        if len(ret) > 1:
            raise ConfigError("invalid device found in file %s" % (cls._FULLFN))
        elif len(ret) == 1:
            item = ret[0]
            if "is_dumb" in item:
                device.set_dumb(pre_ready_call=True)
            if "default_offline_reason" in item:
                device.set_default_offline_reason(OfflineReason(item["default_offline_reason"]), pre_ready_call=True)
            return device
        elif len(ret) == 0:
            return device
        else:
            assert False

    tlist = []
    for item in cls._DEVICES:
        ret = Handy.findDevicesByItem(machine._devices, item)
        if len(ret) > 1:
            raise ConfigError("invalid device found in file %s" % (cls._FULLFN))
        elif len(ret) == 1:
            pass
        elif len(ret) == 0:
            if "vendor" in item:
                ret = Handy.getVendorPlugin(item["vendor"]).get_device(item["model"], None)
            else:
                from .machine import Device
                ret = Device(None, None, None, item["device_type"])
            ret.set_id(item.get("device_id", None))
            if "is_dumb" in item:
                ret.set_dumb(pre_ready_call=True)
            if "default_offline_reason" in item:
                ret.set_default_offline_reason(OfflineReason(item["default_offline_reason"]), pre_ready_call=True)
            tlist.append(ret)
        else:
            assert False
    if len(tlist) > 0:
        machine._addDevices(tlist)

    buf += "\n"

    buf += "[device]\n"
    for c in machine._devices:
        item = dict()
        if c.get_vendor() is not None:
            item["vendor"] = c.get_vendor()
            item["model"] = c.get_model()
        else:
            item["device_type"] = c.get_type()
        if c.get_id() is not None:
            item["id"] = c.get_id()
        buf += json.dumps(item, indent=4)
        buf += "\n"
    buf += "\n"


    if section == "[device]":
        if line == "{":
            if objStr is not None:
                raise ConfigError("invalid line %d in file %s" % (i, cls._FULLFN))
            objStr = line + "\n"
            continue
        if line == "}":
            if objStr is None:
                raise ConfigError("invalid line %d in file %s" % (i, cls._FULLFN))
            objStr += line + "\n"
            item = json.loads(objStr)
            __checkDevice(item)
            cls._DEVICES.append(item)
            continue
        if objStr is not None:
            objStr += line + "\n"
        continue



    def __checkDevice(item):
        vendor = item.get("vendor", None)
        model = item.get("model", None)
        deviceType = item.get("device_type", None)

        if vendor is not None:
            if model is not None:
                if deviceType is not None:
                    raise ConfigError("invalid device found in file %s" % (cls._FULLFN))
            else:
                raise ConfigError("invalid device found in file %s" % (cls._FULLFN))
        else:
            if model is not None:
                raise ConfigError("invalid device found in file %s" % (cls._FULLFN))
            else:
                if deviceType is None:
                    raise ConfigError("invalid device found in file %s" % (cls._FULLFN))

        if "is_dumb" in item:
            if not item["is_dumb"]:
                raise ConfigError("invalid device found in file %s" % (cls._FULLFN))

        if "default_offline_reason" in item:
            if not isinstance(item["default_offline_reason"], OfflineReason):
                raise ConfigError("invalid device found in file %s" % (cls._FULLFN))




        devices = []
        for item in cls._DEVICES:
            ret = Handy.findDevicesByItem(machine._devices, item)
            if len(ret) > 1:
                raise ConfigError("invalid device found in file %s" % (cls._FULLFN))
            elif len(ret) == 1:
                pass
            elif len(ret) == 0:
                if "vendor" in item:
                    ret = Handy.getVendorPlugin(item["vendor"]).get_device(item["model"], None)
                else:
                    from .machine import Device
                    ret = Device(None, None, None, item["device_type"])
                ret.set_id(item.get("device_id", None))
                devices.append(ret)
            else:
                assert False



    def add_components(self, components, pre_ready_call=False):
        assert self.__readyCheck(pre_ready_call)
        assert len(components) > 0

        # pre-add check
        for component in components:
            assert isinstance(component, Component)
            assert component._parent is None
            # assert component.get_seat() is None or component.get_seat() in self._seats

        # do add
        for component in components:
            self._components.append(component)
            component._parent = self
            component._callEvaluate()
            for d in component._getDevices():
                d._callEvaluate()

        # post-add check
        for component in components:
            assert component.is_dumb() or not component.is_online()                         # components that are not dumb nor offline should be auto detected, not added
        assert not MachineUtil.hasDuplicateIds(self)
        assert not MachineUtil.hasDuplicateNames(self)

        # write config file
        if not pre_ready_call:
            ComponentsFile.write(self._components, self._devices)












    def is_category(self, category):
        assert False


class Machine(PluginComponent):

    def get_all_peripherals(self):
        assert self._ready

        ret = []
        for c in self._components:
            if not c.is_peripheral():
                continue
            ret.append(c)
        return ret

    def get_peripherals(self):
        return [x for x in self.get_all_peripherals() if not x.is_disabled() and not c.is_removed()]






class DumbComponentsFile:

    """
    Example:
    [
        {
            "vendor": "xxx",
            "model": "yyy",
            "id": "xxx",
        }
        {
            "vendor": "xxx",
            "model": "yyy",
        }
        {
            "component_type": "printer",
            "id": "xxx",
        }
        {
            "component_type": "printer",
        }
    ]
    """

    PATH = "/etc/udev/dumb-components.json"

    @classmethod
    def _read(cls):
        components = []
        devices = []

        if not os.path.exists(cls.PATH):
            return (components, devices)

        def __checkComponent(item):
            vendor = item.get("vendor", None)
            model = item.get("model", None)
            componentType = item.get("component_type", None)

            if vendor is not None:
                if model is not None:
                    if componentType is not None:
                        raise ConfigError("invalid component found in file %s" % (cls.PATH))
                else:
                    raise ConfigError("invalid component found in file %s" % (cls.PATH))
            else:
                if model is not None:
                    raise ConfigError("invalid component found in file %s" % (cls.PATH))
                else:
                    if componentType is None:
                        raise ConfigError("invalid component found in file %s" % (cls.PATH))

            if "is_dumb" in item:
                if not item["is_dumb"]:
                    raise ConfigError("invalid component found in file %s" % (cls.PATH))

            if "default_offline_reason" in item:
                if not isinstance(item["default_offline_reason"], OfflineReason):
                    raise ConfigError("invalid component found in file %s" % (cls.PATH))

        def __checkDevice(item):
            vendor = item.get("vendor", None)
            model = item.get("model", None)
            deviceType = item.get("device_type", None)

            if vendor is not None:
                if model is not None:
                    if deviceType is not None:
                        raise ConfigError("invalid device found in file %s" % (cls.PATH))
                else:
                    raise ConfigError("invalid device found in file %s" % (cls.PATH))
            else:
                if model is not None:
                    raise ConfigError("invalid device found in file %s" % (cls.PATH))
                else:
                    if deviceType is None:
                        raise ConfigError("invalid device found in file %s" % (cls.PATH))

            if "is_dumb" in item:
                if not item["is_dumb"]:
                    raise ConfigError("invalid device found in file %s" % (cls.PATH))

            if "default_offline_reason" in item:
                if not isinstance(item["default_offline_reason"], OfflineReason):
                    raise ConfigError("invalid device found in file %s" % (cls.PATH))

        lines = pathlib.Path(cls.PATH).read_text().split("\n")
        section = None
        objStr = None
        for i in range(0, len(lines)):
            line = lines[i]
            if line in ["[component]", "[device]"]:
                section = line
                continue
            if line == "":
                section = None
                continue

            if section == "[component]":
                if line == "{":
                    if objStr is not None:
                        raise ConfigError("invalid line %d in file %s" % (i, cls.PATH))
                    objStr = line + "\n"
                    continue
                if line == "}":
                    if objStr is None:
                        raise ConfigError("invalid line %d in file %s" % (i, cls.PATH))
                    objStr += line + "\n"
                    item = json.loads(objStr)
                    __checkComponent(item)
                    components.append(item)
                    continue
                if objStr is not None:
                    objStr += line + "\n"
                continue

            if section == "[device]":
                if line == "{":
                    if objStr is not None:
                        raise ConfigError("invalid line %d in file %s" % (i, cls.PATH))
                    objStr = line + "\n"
                    continue
                if line == "}":
                    if objStr is None:
                        raise ConfigError("invalid line %d in file %s" % (i, cls.PATH))
                    objStr += line + "\n"
                    item = json.loads(objStr)
                    __checkDevice(item)
                    devices.append(item)
                    continue
                if objStr is not None:
                    objStr += line + "\n"
                continue

        return (components, devices)

    @classmethod
    def generate(cls, machine):
        buf = ""

        for c in machine._components:
            item = dict()
            if c.get_vendor() is not None:
                item["vendor"] = c.get_vendor()
                item["model"] = c.get_model()
            item["component_type"] = c.get_type()
            if c.get_id() is not None:
                item["id"] = c.get_id()
            buf += json.dumps(item, indent=4)
            buf += "\n"
        buf += "\n"

        return (cls.PATH, buf)

    @classmethod
    def write(cls, machine):
        fullfn, buf = cls.generate(machine)
        os.makedirs(os.path.dirname(fullfn), exist_ok=True)
        with open(fullfn, "w") as f:
            f.write(buf)





    @classmethod
    def _read(cls):
        if cls._DATA_MAP is not None:
            return

        cls._DATA_MAP = {
            "component": [],
            "device": [],
            "connector": []
        }

        if not os.path.exists(cls.PATH):
            return

        lines = pathlib.Path(cls.PATH).read_text().split("\n")
        objStr = None
        for i in range(0, len(lines)):
            line = lines[i].strip()

            if line == "{":
                if objStr is not None:
                    raise ConfigError("invalid line %d in file %s" % (i, cls.PATH))
                objStr = line + "\n"
                continue

            if line == "}":
                if objStr is None:
                    raise ConfigError("invalid line %d in file %s" % (i, cls.PATH))
                objStr += line + "\n"
                obj = json.loads(objStr)
                if "match_component" in obj:
                    obj["match"] = obj.pop("match_component")
                    cls._DATA_MAP["component"].append(obj)
                elif "match_device" in obj:
                    obj["match"] = obj.pop("match_device")
                    cls._DATA_MAP["device"].append(obj)
                elif "match_connector" in obj:
                    obj["match"] = obj.pop("match_connector")
                    cls._DATA_MAP["connector"].append(obj)
                else:
                    raise ConfigError("invalid line %d in file %s" % (i, cls.PATH))
                objStr = None
                continue

            if objStr is None:
                if line == "" or line.startswith("#"):
                    continue
                else:
                    raise ConfigError("invalid line %d in file %s" % (i, cls.PATH))

            objStr += line + "\n"

        if objStr is not None:
            raise ConfigError("invalid content in file %s" % (cls.PATH))



    def modify_component(cls, component):
        cls._read()

        m = MatchComponent()
        for item in data["component"]:
            m.add_expression(item["mactcher"], functools.partial(cls._modifyComponent, item))
        m.evaluate(component)

        return component

    def modify_device(cls, device):
        cls._read()

        m = MatchDevice()
        for item in data["device"]:
            m.add_expression(item["mactcher"], functools.partial(cls._modifyDevice, item))
        m.evaluate(device)

        return device

    def modify_connector(cls, connector):
        cls._read()

        m = MatchConnector()
        for item in data["connector"]:
            m.add_expression(item["mactcher"], functools.partial(cls._modifyConnector, item))
        m.evaluate(connector)

        return connector

    def _modifyComponent(cls, item, component):
        if "is_dumb" in item:
            if not item["is_dumb"]:
                raise ConfigError("invalid component found in file %s" % (cls.PATH))
            component.set_dumb(pre_ready_call=True)

        if "default_offline_reason" in item:
            if not isinstance(item["default_offline_reason"], OfflineReason):
                raise ConfigError("invalid component found in file %s" % (cls.PATH))
            component.set_default_offline_reason(item["default_offline_reason"], pre_ready_call=True)

    def _modifyConnector(cls, item, connector):
        if "peer_connector_matcher" in item:
            connector.set_peer_connector_matcher(item["peer_connector_matcher"], pre_ready_call=True)
